package snuvm.venus;

import snuvm.*;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.undo.*;
import java.text.*;
import java.util.*;
import java.io.*;

/*
Copyright (c) 2009-2010,  Jeongho Nah, Gangwon Jo, Jaejin Lee

Developed by 
	Jeongho Nah (jeongho"at"aces.snu.ac.kr)
	Gangwon Jo (gangwon"at"aces.snu.ac.kr)
	Jaejin Lee (jlee"at"cse.snu.ac.kr)


Permission is hereby granted to use this version of the code 
under the same terms as MARS, which has the following license:

--
Copyright (c) 2003-2008,  Pete Sanderson and Kenneth Vollmar

Developed by Pete Sanderson (psanderson"at"otterbein.edu)
and Kenneth Vollmar (kenvollmar"at"missouristate.edu)

Permission is hereby granted, free of charge, to any person obtaining 
a copy of this software and associated documentation files (the 
"Software"), to deal in the Software without restriction, including 
without limitation the rights to use, copy, modify, merge, publish, 
distribute, sublicense, and/or sell copies of the Software, and to 
permit persons to whom the Software is furnished to do so, subject 
to the following conditions:

The above copyright notice and this permission notice shall be 
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

(MIT license, http://www.opensource.org/licenses/mit-license.html)
 */

	/**
	  *  Creates the tabbed areas in the UI and also created the internal windows that 
	  *  exist in them.
	  *   @author Sanderson and Bumgarner
	  **/

    public class EditPane extends JPanel implements Observer {
   
      private JTextArea sourceCode;
      private VenusUI mainUI;
      private UndoManager undo;
      private String currentDirectoryPath;
      private JLabel caretPositionLabel;
      private JCheckBox showLineNumbers;
      private JLabel lineNumbers;
      private static final char newline = '\n';
      private static int count = 0;
      private boolean isCompoundEdit = false;
      private CompoundEdit compoundEdit;
   
    /**
      *  Constructor for the EditPane class. 
   	**/
   	
       public EditPane(VenusUI appFrame){
         super(new BorderLayout());
         this.mainUI = appFrame;
         currentDirectoryPath = System.getProperty("user.dir","c:\\");
         undo = new UndoManager();
         mainUI.editor = new Editor(mainUI);
      	// We want to be notified of editor font changes! See update() below.
         Globals.getSettings().addObserver(this);
      
         this.sourceCode = new JTextArea();
         this.sourceCode.setFont(Globals.getSettings().getEditorFont());
         this.sourceCode.setTabSize(4);
         this.sourceCode.setMargin(new Insets(0,3,3,3));
      	// If source code is modified, will set flag to trigger/request file save.
         sourceCode.getDocument().addDocumentListener(
                new DocumentListener() {
                   public void insertUpdate(DocumentEvent evt) {
                     FileStatus.setEdited(true);
                     switch (FileStatus.get()) {
                        case FileStatus.NEW_NOT_EDITED:
                           FileStatus.set(FileStatus.NEW_EDITED);
                           break;
                        case FileStatus.NEW_EDITED:
                           break;
                        default:
                           FileStatus.set(FileStatus.EDITED);
                     }
                     mainUI.editor.setFrameTitle();
                     if (showingLineNumbers()) {
                        lineNumbers.setText(getLineNumbersList());
                     }
                  }
                   public void removeUpdate(DocumentEvent evt) {
                     this.insertUpdate(evt);
                  }
                   public void changedUpdate(DocumentEvent evt) {
                     this.insertUpdate(evt);
                  }
               });
      	// Needed to support unlimited undo/redo capability
         sourceCode.getDocument().addUndoableEditListener(
                new UndoableEditListener() {
                   public void undoableEditHappened(UndoableEditEvent e) {
                  //Remember the edit and update the menus.
                     if (isCompoundEdit) {
                        compoundEdit.addEdit(e.getEdit());
                     } 
                     else {
                        undo.addEdit(e.getEdit());
                        mainUI.editUndoAction.updateUndoState();
                        mainUI.editRedoAction.updateRedoState();
                     }
                  }
               });
      	
         sourceCode.getCaret().addChangeListener(
                new ChangeListener() {
                   public void stateChanged(ChangeEvent e) {
                     displayCaretPosition(convertStreamPositionToLineColumn(sourceCode.getCaretPosition()));
                  }
               });
         this.setSourceCode("",false);
         
      	
         lineNumbers = new JLabel();
         lineNumbers.setFont(getLineNumberFont(sourceCode.getFont()));
         //lineNumbers.setForeground(Color.RED);
      	//lineNumbers.setBackground(Color.WHITE);
         lineNumbers.setVerticalAlignment(JLabel.TOP);
         lineNumbers.setText("");
         lineNumbers.setVisible(false);
         showLineNumbers = new JCheckBox("Show Line Numbers");
         showLineNumbers.setToolTipText("If checked, will display line number for each line of text.");
         showLineNumbers.setEnabled(false);
      	// Show line numbers by default.
         showLineNumbers.setSelected(Globals.getSettings().getEditorLineNumbersDisplayed()); 
         lineNumbers.setVisible(true);
      	
      	// Listener fires when "Show Line Numbers" check box is clicked.
         showLineNumbers.addItemListener(
                new ItemListener() {
                   public void itemStateChanged(ItemEvent e) {
                     if (showLineNumbers.isSelected()) {
                        lineNumbers.setText(getLineNumbersList());
                        lineNumbers.setVisible(true);
                     } 
                     else {
                        lineNumbers.setVisible(false);
                     }
                     Globals.getSettings().setEditorLineNumbersDisplayed(showLineNumbers.isSelected());
                  	// needed because caret disappears when checkbox clicked
                     setCursorVisible(true); 
                     sourceCode.requestFocusInWindow();
                  }
               });
         JPanel source = new JPanel(new BorderLayout());
         source.add(lineNumbers,BorderLayout.WEST);
         source.add(sourceCode,BorderLayout.CENTER);
         JScrollPane editAreaScrollPane = new JScrollPane(source, 
                       ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, 
                       ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
         editAreaScrollPane.getVerticalScrollBar().setUnitIncrement(
                       sourceCode.getFontMetrics(this.sourceCode.getFont()).getHeight());
         this.add(editAreaScrollPane, BorderLayout.CENTER);
         JPanel editInfo = new JPanel(new BorderLayout());
         caretPositionLabel = new JLabel();
         caretPositionLabel.setToolTipText("Tracks the current position of the text editing cursor.");
         displayCaretPosition(new Point());
         editInfo.add(caretPositionLabel,BorderLayout.WEST);
         editInfo.add(showLineNumbers,BorderLayout.CENTER);
         this.add(editInfo,BorderLayout.SOUTH);
      }
      
   	/**
   	  *  For initalizing the source code when opening an ASM file
   	  *   @param s String set to null from JSPIMMenu.
   	  *   @param editable set true if code is editable else false
   	  **/
   	
       public void setSourceCode(String s, boolean editable){
         this.sourceCode.setText(s);
         this.sourceCode.setBackground( (editable)? Color.WHITE : Color.GRAY);
         this.sourceCode.setEditable(editable);  
         this.sourceCode.setEnabled(editable);
         this.sourceCode.getCaret().setVisible(editable);
         this.sourceCode.setCaretPosition(0);
         if (editable) this.sourceCode.requestFocusInWindow();
      }
      
   
   	/** Form string with source code line numbers.
   	 * Resulting string is HTML, for which JLabel will happily honor <br> to do
   	 * multiline label (it ignores '\n').  The line number list is a JLabel with
   	 * one line number per line.
   	 */
       public String getLineNumbersList() {
         StringBuffer lineNumberList = new StringBuffer("<html>");
         int lineCount = this.getSourceLineCount();
         for (int i=1; i<=lineCount;i++) {
            lineNumberList.append(""+i+"<br>");
         }
         lineNumberList.append("</html>");
         return lineNumberList.toString();
      }
   
   
      /** Calculate and return number of lines in source code text.
   	 * Do this by counting newline characters then adding one if last line does
   	 * not end with newline character.
   	 */
   	 
   	/*  IMPLEMENTATION NOTE:
   	 * Tried repeatedly to use StringTokenizer to count lines but got bad results 
   	 * on empty lines (consecutive delimiters) even when returning delimiter as token.
   	 * BufferedReader on StringReader seems to work better.
   	 */
       public int getSourceLineCount() {
         BufferedReader bufStringReader = new BufferedReader(new StringReader(sourceCode.getText()));
         int lineNums = 0;
         try {
            while (bufStringReader.readLine() != null) {
               lineNums++;
            }
         }
             catch (IOException e) {
            }
         return lineNums;
      }
   	
   	/**
   	  *  Adds the source code line by line.
   	  *   @param s A line of source code.
   	  **/
      
       public void append(String s){ 
         this.sourceCode.append(s);
         this.sourceCode.setCaretPosition(0);
      }
   
      /**
   	 * Get source code text
   	 *
   	 * @return Sting containing source code
   	 */
       public String getSource(){
         return sourceCode.getText();
      }
   	
   	/**
   	 *  Get the current directory path
   	 *
   	 * @return String containing current directory pathname
   	 */
       public String getCurrentDirectoryPath() {
         return currentDirectoryPath;
      }
   	
   	/**
   	 *  Set the current directory pathname
   	 *  
   	 * @param path the desired directory pathname
   	 */
       public void setCurrentDirectoryPath(String path) {
         currentDirectoryPath = path;
      }
   	
      /**
   	 *  get the manager in charge of UnDo operations
   	 *  @return the UnDo manager
   	 */
       public UndoManager getUndoManager() {
         return undo;
      }
      /*       Note: these are invoked only when copy/cut/paste are used from the
   	               toolbar or menu or the defined menu Alt codes.  When
   						Ctrl-C, Ctrl-X or Ctrl-V are used, this code is NOT invoked
   						but the operation works correctly!
   				The "set visible" operations are used because clicking on the toolbar
   				icon causes both the selection highlighting AND the blinking cursor
   				to disappear!  This does not happen when using menu selection or 
   				Ctrl-C/X/V
   	*/
   	/**
   	 * copy currently-selected text into clipboard
   	 */
       public void copyText() {
         sourceCode.copy();
         setCursorVisible(true);
         sourceCode.getCaret().setSelectionVisible(true);
      }
   	 
   	/**
   	 * cut currently-selected text into clipboard
   	 */
       public void cutText() {
         sourceCode.cut();		 
         setCursorVisible(true);
      }
   	/**
   	 * paste clipboard contents at cursor position
   	 */  
       public void pasteText() {
         sourceCode.paste();		 
         setCursorVisible(true);
      }		 
   	 
   	 /**
   	  * Control cursor visibility
   	  *
   	  * @param vis true to display cursor, false to hide it
   	  */
       public void setCursorVisible(boolean vis) {
         sourceCode.getCaret().setVisible(vis);
      }
   	 
   	/**
   	 *  get editor's line number display status 
   	 *
   	 *  @return true if editor is current displaying line numbers, false otherwise.
   	 */
       public boolean showingLineNumbers() {
         return showLineNumbers.isSelected();
      }
   
   	/**
   	 *  enable or disable checkbox that controls display of line numbers 
   	 *
   	 *  @param enable True to enable box, false to disable.
   	 */   	 
       public void setShowLineNumbersEnabled(boolean enabled) {
         showLineNumbers.setEnabled(enabled);
         //showLineNumbers.setSelected(false); // set off, whether closing or opening
      }
   	 /**
   	  *  Display cursor coordinates
   	  *
   	  *  @param p  Point object with x-y (column, line number) coordinates of cursor
   	  */
       public void displayCaretPosition(Point p) {
         caretPositionLabel.setText("Line: "+ p.y + " Column: "+ p.x);
      }
   
   //////////////////////////////////////////////////////////////////////////
   //  Methods to support Find/Replace feature
   //
   // Basis for this Find/Replace solution is:
   // http://java.ittoolbox.com/groups/technical-functional/java-l/search-and-replace-using-jtextpane-630964
   // as written by Chris Dickenson in 2005
   //
   // sourceCode is implemented as JTextArea rather than JTextPane but the necessary methods are inherited
   // by both from JTextComponent.
   
      public static final int TEXT_NOT_FOUND = 0;
      public static final int TEXT_FOUND = 1;
      public static final int TEXT_REPLACED_FOUND_NEXT = 2;
      public static final int TEXT_REPLACED_NOT_FOUND_NEXT = 3;
   
    /** Finds next occurrence of text in a forward search of a string. Search begins 
     * at the current cursor location, and wraps around when the end of the string
     * is reached.
     * @param find the text to locate in the string 
     * @param caseSensitive true if search is to be case-sensitive, false otherwise
     * @return TEXT_FOUND or TEXT_NOT_FOUND, depending on the result.
     */
       public int doFindText(String find, boolean caseSensitive) {
         int findPosn = sourceCode.getCaretPosition();
         int nextPosn = 0;
         nextPosn = nextIndex( sourceCode.getText(), find, findPosn, caseSensitive );
         if ( nextPosn >= 0 ) {
            sourceCode.setSelectionStart( nextPosn ); // position cursor at word start
            sourceCode.setSelectionEnd( nextPosn + find.length() );
            return TEXT_FOUND;
         } 
         else {
            return TEXT_NOT_FOUND;
         }
      }
   	 
   /** Returns next posn of word in text - forward search.  If end of string is
    *  reached during the search, will wrap around to the beginning one time.
   * @return next indexed position of found text or -1 if not found
   * @param input the string to search
   * @param find the string to find
   * @param start the character position to start the search
   * @param caseSensitive true for case sensitive. false to ignore case
   */
       public int nextIndex(String input, String find, int start, boolean caseSensitive ) {
         int textPosn = -1;
         if ( input != null && find != null && start < input.length() ) {
            if ( caseSensitive ) { // indexOf() returns -1 if not found
               textPosn = input.indexOf( find, start );
               // If not found from non-starting cursor position, wrap around
               if (start > 0 && textPosn < 0) {
                  textPosn = input.indexOf( find );
               }
            } 
            else {
               String lowerCaseText = input.toLowerCase();
               textPosn = lowerCaseText.indexOf( find.toLowerCase(), start );
                // If not found from non-starting cursor position, wrap around
               if (start > 0 && textPosn < 0) {
                  textPosn = lowerCaseText.indexOf( find.toLowerCase() );
               }
            }
         }
         return textPosn;
      }
   
   
  /** Finds and replaces next occurrence of text in a string in a forward search.
   * If cursor is initially at end
   *  of matching selection, will immediately replace then find and select the
   *  next occurrence if any.  Otherwise it performs a find operation.  The replace
   *  can be undone with one undo operation.
   *
   * @param find the text to locate in the string
   * @param replace the text to replace the find text with - if the find text exists
   * @param caseSensitive true for case sensitive. false to ignore case
   * @return Returns TEXT_FOUND if not initially at end of selected match and matching
   * occurrence is found.  Returns TEXT_NOT_FOUND if the text is not matched.
   * Returns TEXT_REPLACED_NOT_FOUND_NEXT if replacement is successful but there are 
   * no additional matches.  Returns TEXT_REPLACED_FOUND_NEXT if reaplacement is
   * successful and there is at least one additional match.
   */
       public int doReplace(String find, String replace, boolean caseSensitive) {
         int nextPosn = 0;
         int posn;
         // Will perform a "find" and return, unless positioned at the end of
      	// a selected "find" result.  
         if (find==null || !find.equals(sourceCode.getSelectedText()) ||
             sourceCode.getSelectionEnd()!=sourceCode.getCaretPosition()) {
            return doFindText(find, caseSensitive);
         }
        // We are positioned at end of selected "find".  Rreplace and find next.
         nextPosn = sourceCode.getSelectionStart();
         sourceCode.grabFocus();
         sourceCode.setSelectionStart( nextPosn ); // posn cursor at word start
         sourceCode.setSelectionEnd( nextPosn + find.length() ); //select found text
         isCompoundEdit = true;
         compoundEdit = new CompoundEdit();
         sourceCode.replaceSelection(replace);
         compoundEdit.end();
         undo.addEdit( compoundEdit );
         mainUI.editUndoAction.updateUndoState();
         mainUI.editRedoAction.updateRedoState();
         isCompoundEdit = false;
         sourceCode.setCaretPosition(nextPosn + replace.length());
         if (doFindText(find, caseSensitive) == TEXT_NOT_FOUND) {
            return TEXT_REPLACED_NOT_FOUND_NEXT;
         } 
         else {				
            return TEXT_REPLACED_FOUND_NEXT;
         }
      }
      
  /** Finds and replaces <B>ALL</B> occurrences of text in a string in a forward search.
   *  All replacements are bundled into one CompoundEdit, so one Undo operation will 
  	*  undo all of them.
   * @param find the text to locate in the string
   * @param replace the text to replace the find text with - if the find text exists 
   * @param caseSensitive true for case sensitive. false to ignore case
   * @return the number of occurrences that were matched and replaced.
   */
       public int doReplaceAll(String find, String replace, boolean caseSensitive) {
         int nextPosn = 0;
         int findPosn = 0; // *** begin at start of text
         int replaceCount = 0;
         compoundEdit = null; // new one will be created upon first replacement
         isCompoundEdit = true; // undo manager's action listener needs this
         while (nextPosn >= 0) {
            nextPosn = nextIndex( sourceCode.getText(), find, findPosn, caseSensitive );
            if ( nextPosn >= 0 ) {
               // nextIndex() will wrap around, which causes infinite loop if
            	// find string is a substring of replacement string.  This 
            	// statement will prevent that.
               if (nextPosn < findPosn) { 
                  break;
               }
               sourceCode.grabFocus();
               sourceCode.setSelectionStart( nextPosn ); // posn cursor at word start
               sourceCode.setSelectionEnd( nextPosn + find.length() ); //select found text
               if (compoundEdit == null) {
                  compoundEdit = new CompoundEdit();
               }
               sourceCode.replaceSelection(replace);
               findPosn = nextPosn + replace.length(); // set for next search
               replaceCount++;
            }
         }
         isCompoundEdit = false;
         // Will be true if any replacements were performed
         if (compoundEdit != null) {
            compoundEdit.end();
            undo.addEdit( compoundEdit );
            mainUI.editUndoAction.updateUndoState();
            mainUI.editRedoAction.updateRedoState();
         }
         return replaceCount;
      }
   // 
   /////////////////////////////  End Find/Replace methods ////////////////////////// 
   	 
   	 /**
   	  * Given byte stream position in text being edited, calculate its column and line
   	  * number coordinates.
   	  *
   	  * @param stream position of character
   	  * @return position Its column and line number coordinate as a Point
   	  */
       public Point convertStreamPositionToLineColumn(int position) {
         String textStream = sourceCode.getText();
         int line = 1;
         int column = 1;
         for (int i=0; i<position; i++) {
            if (textStream.charAt(i) == newline) {
               line++;
               column=1;
            } 
            else {
               column++;
            }
         }
         return new Point(column,line);
      }
   	
   	/**
   	 *  Update, if source code is visible, when Font setting changes.
   	 *  This method is specified by the Observer interface.
   	 */
       public void update(Observable fontChanger, Object arg) {
         sourceCode.setFont(Globals.getSettings().getEditorFont());
         sourceCode.revalidate();
      	// We want line numbers to be displayed same size but always PLAIN style.
      	// Easiest way to get same pixel height as source code is to set to same
      	// font family as the source code! It can get a bit complicated otherwise
      	// because different fonts will render the same font size in different
      	// pixel heights.  This is a factor because the line numbers as displayed
      	// in the editor form a separate column from the source code and if the
      	// pixel height is not the same then the numbers will not line up with
      	// the source lines.
         lineNumbers.setFont(getLineNumberFont(sourceCode.getFont()));
         lineNumbers.revalidate();
      }
   	
   	
   	/* Private helper method.
   	 * Determine font to use for editor line number display, given current
   	 * font for source code.
   	 */
       private Font getLineNumberFont(Font sourceFont) {
         return  (sourceCode.getFont().getStyle() == Font.PLAIN) 
                 ? sourceFont
            	  : new Font(sourceFont.getFamily(), Font.PLAIN, sourceFont.getSize());
      }
      
   	
   }